﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.IO.Ports;
using System.Globalization;

namespace GeoPal
{
    /// <summary>
    /// The Gps class handles the serial port and processes incoming NMEA data
    /// </summary>
    public class Gps : IDisposable
    {
        /// <summary>
        /// The Gps.Position class represents the geographical position and additional information
        /// </summary>
        public class Position
        {
            /// <summary>
            /// Geographical longitude
            /// </summary>
            public double longitude;
            /// <summary>
            /// Geographical latitude
            /// </summary>
            public double latitude;
            /// <summary>
            /// Time of fix position
            /// </summary>
            public DateTime time;
            /// <summary>
            /// Speed
            /// </summary>
            public double speed;
            /// <summary>
            /// Direction
            /// </summary>
            public double bearing;
            /// <summary>
            /// Corresponding GPRMC sentence
            /// </summary>
            public string lastGPRMC;
            /// <summary>
            /// Is the position fixed?
            /// </summary>
            public bool isFixed;
        }

        /// <summary>
        /// Object that contains the current position data
        /// </summary>
        public readonly Position position;
        /// <summary>
        /// Object that contains the last reported position data
        /// </summary>
        public Position lastReportedPosition;
        /// <summary>
        /// Threshold of the position delta event
        /// </summary>
        private int positionDeltaTreshold;
        /// <summary>
        /// Serial port object for the GPS receiver
        /// </summary>
        private SerialPort gpsPort;
        /// <summary>
        /// Cummulated GPS buffer
        /// </summary>
        private string instring;
        /// <summary>
        /// Parsed GPS buffer
        /// </summary>
        private string[] gpsString;
        /// <summary>
        /// GPS buffer
        /// </summary>
        private string serialBuffer;

        /// <summary>
        /// Indicates that the position is fixed
        /// </summary>
        /// <param name="sender">Sender of the event</param>
        /// <param name="isFixed">True, if the position is fixed. Otherwise, false.</param>
        public delegate void PositionFixedDelegate(object sender, bool isFixed);
        /// <summary>
        /// Indicates that the position has changed by the specified amount
        /// </summary>
        /// <param name="sender">Sender of the event</param>
        public delegate void PositionDeltaDelegate(object sender);
        /// <summary>
        /// Event of the PositionFixedDelegate
        /// </summary>
        public event PositionFixedDelegate PositionFixedEvent;
        /// <summary>
        /// Event of the PositionDeltaDelegate
        /// </summary>
        public event PositionDeltaDelegate PositionDeltaEvent;

        /// <summary>
        /// Determines that the GPS position is fixed or not
        /// </summary>
        public bool IsFixed
        {
            get { return position.isFixed; }
            set
            {
                if (value != position.isFixed)
                {
                    PositionFixedEvent(this, value);
                }
                position.isFixed = value;
            }
        }

        /// <summary>
        /// Number of satellites that we can actually see
        /// </summary>
        public int satellitesInView;

        /// <summary>
        /// Represents the EN-US culture, used for numers in NMEA sentences
        /// </summary>
        private static CultureInfo nmeaCultureInfo = new CultureInfo("en-US");
        /// <summary>
        /// Used to convert knots into kilometers per hour
        /// </summary>
        private static double kphPerKnot = 1.852;

        /// <summary>
        /// The constructor of the Gps class
        /// </summary>
        /// <param name="settings">Reference to the Settings class</param>
        public Gps(ref Settings settings)
        {
            position = new Position();
            positionDeltaTreshold = settings.UpdateDistance;
            gpsPort = new SerialPort();
            gpsPort.DataBits = 8;
            gpsPort.Parity = Parity.None;
            gpsPort.StopBits = StopBits.One;
            try
            {
                gpsPort.PortName = settings.Port;
                gpsPort.BaudRate = (int)settings.Baud;
                gpsPort.Open();
                gpsPort.DataReceived += new SerialDataReceivedEventHandler(HandleGPSstring);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }

        /// <summary>
        /// Disposes the port
        /// </summary>
        public void Dispose()
        {
            gpsPort.Close();
        }

        /// <summary>
        /// Switches on (opens) the port
        /// </summary>
        /// <returns>True, if it was closed and now it is open. Otherwise, false.</returns>
        public bool SwitchOn()
        {
            if (!gpsPort.IsOpen)
            {
                gpsPort.Open();
                return true;
            }
            return false;
        }

        /// <summary>
        /// Switches off (closes) the port
        /// </summary>
        /// <returns>True, if it was open and now it is closed. Otherwise, false.</returns>
        public bool SwitchOff()
        {
            if (gpsPort.IsOpen)
            {
                gpsPort.Close();
                return true;
            }
            return false;
        }

        /// <summary>
        /// Delegate to handle the event that indicates there is new data in the buffer
        /// </summary>
        /// <param name="s">Sender of the event</param>
        /// <param name="e">Arguments of the event</param>
        private void HandleGPSstring(object s, EventArgs e)
        {
            serialBuffer = gpsPort.ReadExisting();
            if (serialBuffer != null)
            {
                if (serialBuffer.StartsWith("$"))
                {
                    instring = serialBuffer;
                }
                else
                {
                    instring += serialBuffer;
                }

                gpsString = instring.Split();
                
                foreach (string sentence in gpsString) Parse(sentence);
            }
        }

        /// <summary>
        /// Processes sentences from the GPS receiver
        /// </summary>
        /// <param name="sentence">NMEA sentence</param>
        /// <returns>true, if the recognition was successful;
        /// false, if the recognition fails</returns>
        public bool Parse(string sentence)
        {
            // Discard the sentence if its checksum does not match our
            // calculated checksum
            if (!IsValidSentence(sentence)) return false;
            // Look at the first word to decide where to go next
            switch (GetWords(sentence)[0])
            {
                case "$GPRMC":
                    // A "Recommended Minimum" sentence was found!
                    return ParseGprmc(sentence);
                case "$GPGSV":
                    // A "Satellites in View" sentence was recieved
                    return ParseGpgsv(sentence);
/*                case "$GPGSA":
                    return ParseGPGSA(sentence);
                case "$GPGGA":
                    return ParseGPGGA(sentence);*/
                default:
                    // Indicate that the sentence was not recognized
                    return false;
            }
        }

        /// <summary>
        /// Divides a sentence into individual words
        /// </summary>
        /// <param name="sentence">Sentence string</param>
        /// <returns>Array of fields</returns>
        public static string[] GetWords(string sentence)
        {
            //strip off the final * + checksum
            sentence = sentence.Substring(0, sentence.IndexOf("*"));
            //now split it up
            return sentence.Split(',');
        }

        /// <summary>
        /// Determines if the sentence is valid
        /// </summary>
        /// <param name="sentence">Sentence string</param>
        /// <returns>True, if the sentence is valid. Otherwise, false.</returns>
        public static bool IsValidSentence(string sentence)
        {
            // Compare the characters after the asterisk to the calculation
            return sentence.Substring(sentence.IndexOf("*") + 1) == GetChecksum(sentence);
        }

        /// <summary>
        /// Calculates the checksum for a sentence
        /// </summary>
        /// <param name="sentence">Sentence string</param>
        /// <returns>Checksum string</returns>
        public static string GetChecksum(string sentence)
        {
            // Loop through all chars to get a checksum
            int Checksum = 0;
            foreach (char Character in sentence)
            {
                if (Character == '$')
                {
                    // Ignore the dollar sign
                }
                else if (Character == '*')
                {
                    // Stop processing before the asterisk
                    break;
                }
                else
                {
                    // Is this the first value for the checksum?
                    if (Checksum == 0)
                    {
                        // Yes. Set the checksum to the value
                        Checksum = Convert.ToByte(Character);
                    }
                    else
                    {
                        // No. XOR the checksum with this character's value
                        Checksum = Checksum ^ Convert.ToByte(Character);
                    }
                }
            }
            // Return the checksum formatted as a two-character hexadecimal
            return Checksum.ToString("X2");
        }

        /// <summary>
        /// Interprets a "Recommended minimum data" NMEA sentence
        /// </summary>
        /// <param name="sentence">Sentence string</param>
        /// <returns>True, if the parsing was successful. Otherwise, false.</returns>
        public bool ParseGprmc(string sentence)
        {
            // Divide the sentence into words
            string[] Words = GetWords(sentence);
            // Do we have enough values to describe our location?
            if (Words[3] != "" & Words[4] != "" & Words[5] != "" & Words[6] != "")
            {
                // Yes. Extract latitude and longitude
                // Append hours
                double Latitude = Convert.ToDouble(Words[3].Substring(0,2)) + (Convert.ToDouble(Words[3].Substring(2),nmeaCultureInfo) / 60);
                if (Words[4] == "S")
                    Latitude = Latitude * -1;
                double Longitude = Convert.ToDouble(Words[5].Substring(0, 3)) + (Convert.ToDouble(Words[5].Substring(3), nmeaCultureInfo) / 60);
                if (Words[6] == "W")
                    Longitude = Longitude * -1;

                /* If position delta threshold is set */
                if (positionDeltaTreshold > 0)
                {
                    /* And the position delta exceeds it */
                    if (DistanceBetweenCoordinates(Latitude, Longitude, position.latitude, position.longitude) > positionDeltaTreshold)
                    {
                        PositionDeltaEvent(this);
                    }
                }

                position.longitude = Longitude;
                position.latitude = Latitude;
            }
            // Do we have enough values to parse satellite-derived time?
            if (Words[1] != "")
            {
                // Yes. Extract hours, minutes, seconds and milliseconds
                int UtcHours = Convert.ToInt32(Words[1].Substring(0, 2));
                int UtcMinutes = Convert.ToInt32(Words[1].Substring(2, 2));
                int UtcSeconds = Convert.ToInt32(Words[1].Substring(4, 2));
                int UtcMilliseconds = 0;
                // Extract milliseconds if it is available
                if (Words[1].Length > 7)
                {
                    UtcMilliseconds = Convert.ToInt32(Words[1].Substring(7));
                }
                // Now build a DateTime object with all values
                System.DateTime Today = System.DateTime.Now.ToUniversalTime();
                System.DateTime SatelliteTime = new System.DateTime(Today.Year,
                  Today.Month, Today.Day, UtcHours, UtcMinutes, UtcSeconds,
                  UtcMilliseconds);
                // Notify of the new time, adjusted to the local time zone
                position.time = SatelliteTime.ToLocalTime();
            }
            // Do we have enough information to extract the current speed?
            if (Words[7] != "")
            {
                // Yes.  Parse the speed and convert it to MPH
                position.speed = double.Parse(Words[7], nmeaCultureInfo) * kphPerKnot;
            }
            // Do we have enough information to extract bearing?
            if (Words[8] != "")
            {
                // Indicate that the sentence was recognized
                position.bearing = double.Parse(Words[8], nmeaCultureInfo);
            }
            // Does the device currently have a satellite fix?
            if (Words[2] != "")
            {
                switch (Words[2])
                {
                    case "A":
                        IsFixed = true;
                        position.lastGPRMC = sentence;
                        break;
                    case "V":
                        IsFixed = false;
                        break;
                }
            }
            // Indicate that the sentence was recognized
            return true;
        }

        /// <summary>
        /// Interprets a "Satellites in View" NMEA sentence
        /// </summary>
        /// <param name="sentence">Sentence string</param>
        /// <returns>True, if the parsing was successful. Otherwise, false.</returns>
        public bool ParseGpgsv(string sentence)
        {
            int PseudoRandomCode = 0;
            int Azimuth = 0;
            int Elevation = 0;
            int SignalToNoiseRatio = 0;
            // Divide the sentence into words
            string[] Words = GetWords(sentence);
            // Each sentence contains four blocks of satellite information. 
            // Read each block and report each satellite's information
            int Count = 0;
            for (Count = 1; Count <= 4; Count++)
            {
                // Do we have enough values to parse satellitesIinView?
                if (Words[3] != "")
                {
                    satellitesInView = int.Parse(Words[3]);

                }

                // Does the sentence have enough words to analyze?
                if ((Words.Length - 1) >= (Count * 4 + 3))
                {
                    // Yes.  Proceed with analyzing the block. 
                    // Does it contain any information?
                    if (Words[Count * 4] != "" & Words[Count * 4 + 1] != ""
                      & Words[Count * 4 + 2] != "" & Words[Count * 4 + 3] != "")
                    {
                        // Yes. Extract satellite information and report it
                        PseudoRandomCode = System.Convert.ToInt32(Words[Count * 4]);
                        Elevation = Convert.ToInt32(Words[Count * 4 + 1]);
                        Azimuth = Convert.ToInt32(Words[Count * 4 + 2]);
                        SignalToNoiseRatio = Convert.ToInt32(Words[Count * 4 + 3]);
                        // Notify of this satellite's information
/*                        if (SatelliteReceived != null)
                            SatelliteReceived(PseudoRandomCode, Azimuth,
                            Elevation, SignalToNoiseRatio);*/
                    }
                }
            }
            // Indicate that the sentence was recognized
            return true;
        }
/*
        // Interprets a "Fixed Satellites and DOP" NMEA sentence
        public bool ParseGPGSA(string sentence)
        {
            // Divide the sentence into words
            string[] Words = GetWords(sentence);
            // Update the DOP values
            if (Words[15] != "")
            {
                if (PDOPReceived != null)
                    PDOPReceived(double.Parse(Words[15], NmeaCultureInfo));
            }
            if (Words[16] != "")
            {
                if (HDOPReceived != null)
                    HDOPReceived(double.Parse(Words[16], NmeaCultureInfo));
            }
            if (Words[17] != "")
            {
                if (VDOPReceived != null)
                    VDOPReceived(double.Parse(Words[17], NmeaCultureInfo));
            }
            return true;
        }
        // Returns True if a sentence's checksum matches the

        //  Interprets a $GPGGA message
        public bool ParseGPGGA(string sentence)
        {
            // Divide the sentence into words
            string[] Words = GetWords(sentence);
            // Satellites Used
            if (Words[7] != "")
            {
                if (SatellitesUsed != null)
                    SatellitesUsed(int.Parse(Words[7]));
            }
            if (Words[8] != "")
            {
                if (HDOPReceived != null)
                    HDOPReceived(double.Parse(Words[8], NmeaCultureInfo));
            }

            if (Words[9] != "")
            {
                if (EllipsoidHeightReceived != null)
                    EllipsoidHeightReceived(double.Parse(Words[9]));
            }
            //

            return true;
        }
        */
        /// <summary>
        /// Static version of the GPRMC interpreter
        /// </summary>
        /// <param name="sentence">Sentence string</param>
        /// <param name="pos">Position object to store the pasing result</param>
        /// <returns>True, if the parsing was successful. Otherwise, false.</returns>
        public static bool _ParseGprmc(string sentence, ref Position pos)
        {
            // Divide the sentence into words
            string[] Words = GetWords(sentence);
            // Do we have enough values to describe our location?
            if (Words[3] != "" & Words[4] != "" & Words[5] != "" & Words[6] != "")
            {
                // Yes. Extract latitude and longitude
                // Append hours
                double Latitude = Convert.ToDouble(Words[3].Substring(0, 2)) + (Convert.ToDouble(Words[3].Substring(2), nmeaCultureInfo) / 60);
                if (Words[4] == "S")
                    Latitude = Latitude * -1;
                double Longitude = Convert.ToDouble(Words[5].Substring(0, 3)) + (Convert.ToDouble(Words[5].Substring(3), nmeaCultureInfo) / 60);
                if (Words[6] == "W")
                    Longitude = Longitude * -1;

                pos.longitude = Longitude;
                pos.latitude = Latitude;
            }
            // Do we have enough values to parse satellite-derived time?
            if (Words[1] != "")
            {
                // Yes. Extract hours, minutes, seconds and milliseconds
                int UtcHours = Convert.ToInt32(Words[1].Substring(0, 2));
                int UtcMinutes = Convert.ToInt32(Words[1].Substring(2, 2));
                int UtcSeconds = Convert.ToInt32(Words[1].Substring(4, 2));
                int UtcMilliseconds = 0;
                // Extract milliseconds if it is available
                if (Words[1].Length > 7)
                {
                    UtcMilliseconds = Convert.ToInt32(Words[1].Substring(7));
                }
                // Now build a DateTime object with all values
                System.DateTime Today = System.DateTime.Now.ToUniversalTime();
                System.DateTime SatelliteTime = new System.DateTime(Today.Year,
                  Today.Month, Today.Day, UtcHours, UtcMinutes, UtcSeconds,
                  UtcMilliseconds);
                // Notify of the new time, adjusted to the local time zone
                pos.time = SatelliteTime.ToLocalTime();
            }
            // Do we have enough information to extract the current speed?
            if (Words[7] != "")
            {
                // Yes.  Parse the speed and convert it to MPH
                pos.speed = double.Parse(Words[7], nmeaCultureInfo) * kphPerKnot;
            }
            // Do we have enough information to extract bearing?
            if (Words[8] != "")
            {
                // Indicate that the sentence was recognized
                pos.bearing = double.Parse(Words[8], nmeaCultureInfo);
            }
            // Does the device currently have a satellite fix?
            if (Words[2] != "")
            {
                switch (Words[2])
                {
                    case "A":
                        pos.isFixed = true;
                        pos.lastGPRMC = sentence;
                        break;
                    case "V":
                        pos.isFixed = false;
                        break;
                }
            }
            // Indicate that the sentence was recognized
            return true;
        }

        /* Calculates distance between two coordinates */
        private double DistanceBetweenCoordinates(double lat1, double lon1, double lat2, double lon2)
        {
            return 0.0;
        }

    }
}
